#include "callother.h"
#include "compiler_unit.h"

#define ERR_BUF_SIZE    255

CoValue *g_scope_efun;
CoValue *g_scope_function;
CoValue *g_scope_global;
CoValue *g_scope_local;

static CoValue *g_ident_scopes; /* dict: name -> scope */
static CoValue *g_ident_values; /* dict: name -> value */
static int g_current_number_of_locals;
static CoValue *g_local_names;  /* list: names of local variables */
static CoValue *g_local_types;  /* list: types of local variables */
static int g_current_number_of_globals;
static CoValue *g_global_names; /* list: names of global variables */
static CoValue *g_global_types; /* list: types of global variables */
static CoValue *g_strings;      /* list: string constants */
static CoValue *g_functions;    /* list: function definations. */

static AstNode *g_ast_tree;

static struct {
    char *name;
    int id;
    int min_arg;
    int max_arg;
    int type;
} g_efuns[] = {
    { "printf", F_PRINTF, 1, 1, CTYPE_VOID },
};

void Compiler_Init(void)
{
    g_scope_efun = CoString_FromString("efun");
    g_scope_function = CoString_FromString("function");
    g_scope_global = CoString_FromString("global variable");
    g_scope_local = CoString_FromString("local variable");
}

void Compiler_Destruct(void)
{
    CO_DECREF(g_scope_efun);
    CO_DECREF(g_scope_function);
    CO_DECREF(g_scope_global);
    CO_DECREF(g_scope_local);
}

static int get_identifier_value(CoValue *name)
{
    CoValue *val;

    val = CoDict_GetItem(g_ident_values, name);
    return CoInt_AsInt(val);
}

static void set_identifier_value(CoValue *name, int value)
{
    CoValue *val;

    val = CoInt_FromInt(value);
    CoDict_SetItem(g_ident_values, name, val);
    CO_DECREF(val);
}

static void remove_identifier(CoValue *name)
{
        CoDict_DelItem(g_ident_scopes, name);
        CoDict_DelItem(g_ident_values, name);
}

/* TODO: should be called once in Compiler_Init */
static void setup_efuns()
{
    int i;
    CoValue *efun_name;

    for (i = 0; i < sizeof(g_efuns) / sizeof(g_efuns[0]); i++)
    {
        efun_name = CoString_FromString(g_efuns[i].name);
        CoDict_SetItem(g_ident_scopes, efun_name, g_scope_efun);
        set_identifier_value(efun_name, i);
        CO_DECREF(efun_name);
    }
}

CoValue *Compiler_CompileFile(CoValue *mod_name, int fd)
{
    int num_global;
    Code *code;
    CoValue *mod;
    CoValue *name;

    void parse_new_file(int); /* lexer.l */

    g_ident_scopes = CoDict_New();
    g_ident_values = CoDict_New();

    setup_efuns();

    g_current_number_of_locals = 0;
    g_local_names = CoList_New(0);
    g_local_types = CoList_New(0);

    g_current_number_of_globals = 0;
    g_global_names = CoList_New(0);
    g_global_types = CoList_New(0);

    g_strings = CoList_New(0);
    g_functions = CoList_New(0);
    g_ast_tree = 0;

    AstNode_Init();

    parse_new_file(fd);
    code = AstNode_GenerateCode(g_ast_tree);

    num_global = CoList_GET_SIZE(g_global_types);
    mod = CoModule_New(mod_name, g_functions, g_strings, num_global, code->c_start, code->c_end - code->c_start);

    AstNode_FreeCode(code);

    AstNode_FreeAllNodes();

    CO_DECREF(g_local_names);
    CO_DECREF(g_local_types);

    /* FreeAllGlobals inline */
    while (g_current_number_of_globals--)
    {
        name = CoList_GetItem(g_global_names, g_current_number_of_globals);
        debug(COMPILE, "free global name: %s", CoString_AS_STRING(name));
        remove_identifier(name);
    }
    CO_DECREF(g_global_names);
    CO_DECREF(g_global_types);

    CO_DECREF(g_strings);
    CO_DECREF(g_functions);

    CO_DECREF(g_ident_scopes);
    CO_DECREF(g_ident_values);

    return mod;
}

CoValue *Compiler_GetIdentifierScope(CoValue *name)
{
    return CoDict_GetItem(g_ident_scopes, name);
}

void Compiler_FreeAllLocals(void)
{
    CoValue *name;

    while (g_current_number_of_locals--)
    {
        name = CoList_GetItem(g_local_names, g_current_number_of_locals);
        debug(COMPILE, "free local name: %s", CoString_AS_STRING(name));
        remove_identifier(name);
    }

    CO_DECREF(g_local_names);
    CO_DECREF(g_local_types);

    g_current_number_of_locals = 0;
    g_local_names = CoList_New(0);
    g_local_types = CoList_New(0);
}

void Compiler_PopGlobals(int num)
{
    CoValue *name;

    assert(num <= g_current_number_of_globals);
    g_current_number_of_globals -= num;

    while (num--) {
        name = CoList_GET_ITEM(g_global_names, g_current_number_of_globals + num);
        debug(COMPILE, "pop global name: %s", CoString_AS_STRING(name));
        remove_identifier(name);
    }
}

void Compiler_PopLocals(int num)
{
    CoValue *name;

    assert(num <= g_current_number_of_locals);
    g_current_number_of_locals -= num;

    while (num--) {
        name = CoList_GET_ITEM(g_local_names, g_current_number_of_locals + num);
        debug(COMPILE, "pop local name: %s", CoString_AS_STRING(name));
        remove_identifier(name);
    }
}

int Compiler_AddGlobal(CoValue *name, int type)
{
    int total_number_of_globals;
    CoValue *val;

    debug(COMPILE, "add global name: %s", CoString_AS_STRING(name));

    total_number_of_globals = CoList_GET_SIZE(g_global_types);

    CoDict_SetItem(g_ident_scopes, name, g_scope_global);
    set_identifier_value(name, total_number_of_globals);

    val = CoInt_FromInt(type);
    CoList_Append(g_global_types, val);
    CO_DECREF(val);

    if (g_current_number_of_globals < total_number_of_globals) {
        CoList_SetItem(g_global_names, g_current_number_of_globals-1, name);
        CO_INCREF(name);
    }
    else {
        CoList_Append(g_global_names, name);
    }
    g_current_number_of_globals++;

    return total_number_of_globals;
}

int Compiler_AddLocal(CoValue *name, int type)
{
    int total_number_of_locals;
    CoValue *val;

    debug(COMPILE, "add local name: %s", CoString_AS_STRING(name));

    total_number_of_locals = CoList_GET_SIZE(g_local_types);

    CoDict_SetItem(g_ident_scopes, name, g_scope_local);
    set_identifier_value(name, total_number_of_locals);

    val = CoInt_FromInt(type);
    CoList_Append(g_local_types, val);
    CO_DECREF(val);

    if (g_current_number_of_locals < total_number_of_locals) {
        CoList_SetItem(g_local_names, g_current_number_of_locals-1, name);
        CO_INCREF(name);
    }
    else {
        CoList_Append(g_local_names, name);
    }
    g_current_number_of_locals++;

    return total_number_of_locals;
}

AstNode *Compiler_UseLocal(CoValue *name)
{
    int index;
    int type;
    CoValue *val;

    index = get_identifier_value(name);
    val = CoList_GetItem(g_local_types, index);
    type = CoInt_AsInt(val);

    return AstNode_CreateOpCodeWithOneArg(F_LOCAL, type, index);
}

AstNode *Compiler_UseGlobal(CoValue *name)
{
    int index;
    int type;
    CoValue *val;

    index = get_identifier_value(name);
    val = CoList_GetItem(g_global_types, index);
    type = CoInt_AsInt(val);

    return AstNode_CreateOpCodeWithOneArg(F_GLOBAL, type, index);
}

int Compiler_AddStringConst(CoValue *vstr)
{
    int index;

    index = CoList_GET_SIZE(g_strings);
    CoList_Append(g_strings, vstr);

    debug(COMPILE, "add string const: %d, %s", index, CoString_AS_STRING(vstr));

    return index;
}

/* The semantics of the flags are currently as follows:

   (1) A function is either a definition, a prototype

   (2) A definition is an address that executes code when
       it is jumped to and finishes with a return.
       The address is labeled by the following info:
       function name, type, flags,number of arguments, number of locals,
       argument types.

   (3) A prototype does not execute code, and is basically a label with
       the following info:
       argument types (optionally also argument names),function name,
       type, flags, number of arguments
 */
#define FUNC_PROTOTYPE  1

#define FUNC_FLAG_BITS  4
#define FUNC_FLAG_MASK  0xf

void Compiler_DeclareNewFunction(CoValue *name, int type, int num_arg)
{
    int index;
    CoValue *func;

    index = CoList_GET_SIZE(g_functions);

    func = CoFunc_New(name, type, num_arg);
    CoList_Append(g_functions, func);
    CO_DECREF(func);

    debug(COMPILE, "declare new function: %s, type = %d, num_arg = %d, index = %d",
                CoString_AS_STRING(name), type, num_arg, index);

    CoDict_SetItem(g_ident_scopes, name, g_scope_function);
    set_identifier_value(name, (index << FUNC_FLAG_BITS) | FUNC_PROTOTYPE);
}

void Compiler_CheckFunctionDeclaration(CoValue *name, int type, int num_arg)
{
    int index;
    int flags;
    CoValue *func;

    index = get_identifier_value(name);
    flags = index & FUNC_FLAG_MASK;
    index = index >> FUNC_FLAG_BITS;
    func = CoList_GetItem(g_functions, index);

    if (flags & FUNC_PROTOTYPE) {
        debug(COMPILE, "update function: %s, type = %d, num_arg = %d, index = %d",
                    CoString_AS_STRING(name), type, num_arg, index);

        if (CoFunc_GET_NUM_ARG(func) != num_arg) {
            Compiler_Error("Number of arguments disagrees with previous declaration.");
        }
        if (CoFunc_GET_TYPE(func) != type) {
            Compiler_Error("Return type of function disagrees with previous declaration.");
        }

        /* TODO: check arg types */

        CoFunc_UpdateDeclaration(func, type, num_arg);
    }
}

void Compiler_DefineFunction(CoValue *name, AstNode *node)
{
    int index;
    int flags;
    int num_local_and_arg;
    int num_local;
    Code *code;
    CoValue *func;

    index = get_identifier_value(name);
    flags = index & FUNC_FLAG_MASK;
    index = index >> FUNC_FLAG_BITS;
    func = CoList_GetItem(g_functions, index);

    if (!(flags & FUNC_PROTOTYPE)) {
            char buf[ERR_BUF_SIZE + 2];
            snprintf(buf, ERR_BUF_SIZE, "Redeclaration of function '%s'",
                    CoString_AS_STRING(name));
            Compiler_Error(buf);
            return;
    }

    set_identifier_value(name, (index << FUNC_FLAG_BITS));

    num_local_and_arg = CoList_GET_SIZE(g_local_types);
    num_local = num_local_and_arg - CoFunc_GET_NUM_ARG(func);

    /*
       For safety, always append a return node as the right-most child of the AST tree of function.
     */
    if (node) {
        node = AstNode_CreateTwoValues(0, node, AstNode_CreateReturn(0));
    }
    else {
        node = AstNode_CreateReturn(0);
    }

    code = AstNode_GenerateCode(node);

    CoFunc_UpdateDefination(func, num_local, code->c_start, code->c_end - code->c_start);

    AstNode_FreeCode(code);

    debug(COMPILE, "define function: %s, num_local = %d, index = %d",
                CoString_AS_STRING(name), num_local, index);
}

AstNode *Compiler_CallEFun(CoValue *name, AstNode *node)
{
    int index;
    int num_arg;
    int min_arg;
    int max_arg;

    num_arg = node->v.number;
    index = get_identifier_value(name);
    min_arg = g_efuns[index].min_arg;
    max_arg = g_efuns[index].max_arg;

    if (num_arg < min_arg)
    {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Too few arguments to efun '%s'", CoString_AS_STRING(name));
        AstNode_SetError(node);
        Compiler_Error(buf);
        return node;
    }
    else if (max_arg != -1 && num_arg > max_arg)
    {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Too many arguments to efun '%s'", CoString_AS_STRING(name));
        AstNode_SetError(node);
        Compiler_Error(buf);
        return node;
    }

    return AstNode_CallEFun(node, g_efuns[index].id, num_arg, g_efuns[index].type);
}

AstNode *Compiler_CallFunction(CoValue *name, AstNode *node)
{
    int index;
    CoValue *func;
    int num_arg;

    debug(COMPILE, "call function: %s", CoString_AS_STRING(name));

    index = get_identifier_value(name) >> FUNC_FLAG_BITS;
    func = CoList_GetItem(g_functions, index);

	/* Check number of arguments. */
    num_arg = node->v.number;
    if (CoFunc_GET_NUM_ARG(func) != num_arg) {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Wrong number of argument to function '%s'\n    Expected: %d   Got: %d",
                CoString_AS_STRING(name), CoFunc_GET_NUM_ARG(func), num_arg);
        Compiler_Error(buf);
    }

    /* TODO: check argument types */

    return AstNode_CallFunction(node, index, CoFunc_GET_TYPE(func));
}

void Compiler_SetAstTree(AstNode *node)
{
    debug(COMPILE, "set tree");

    /*
       Always append a return node as the right-most child of the AST tree of program.
     */
    if (node) {
        node = AstNode_CreateTwoValues(0, node, AstNode_CreateReturn(0));
    }
    else {
        node = AstNode_CreateReturn(0);
    }

    g_ast_tree = node;
}

void Compiler_Error(char *s)
{
    fprintf(stderr, "%s\n", s);
}
